import fs from 'fs-extra'
import path from 'path'
import { Color, Layer, Psd, TextStyle } from 'ag-psd'
import env from './Env.js'

export interface ILayerWithCanvas extends Layer {
    pngFile: string
}

declare type PrefabNodeType = 'GameObject' | 'Image' | 'Text'

interface ISimpleNode {
    name?: string
    left?: number
    right?: number
    top?: number
    bottom?: number
    children: ISimpleNode[]
}

interface IBaseNode {
    name?: string
    width: number
    height: number
    x: number
    y: number
    children: IPrefabNode[]
}

export interface IPrefabInstruction extends IBaseNode {
    name: string
}

export interface IPrefabNode extends IBaseNode {
    type: PrefabNodeType
}

export interface IPrefabGameObject extends IPrefabNode {
    type: 'GameObject'
}

export interface IPrefabImage extends IPrefabNode {
    type: 'Image'
    image: string
}

interface ITextStyle {
    font?: string
    fontSize?: number
    color?: Color
    strokeColor?: Color
}

interface IStyleRun {
    length: number
    style: ITextStyle
}

interface IPrefabTextDetail {
    text: string
    style?: ITextStyle
    styleRuns?: IStyleRun[]
}

export interface IPrefabText extends IPrefabNode {
    type: 'Text'
    text: IPrefabTextDetail
}

export class PrefabGenerator {
    public async make (psd: Psd): Promise<void> {
        const instruction: IPrefabInstruction = {
            name: env.opts.name,
            children: [],
            width: psd.width,
            height: psd.height,
            x: 0,
            y: 0
        }
        const simStruct: { children: ISimpleNode[] } = {
            children: []
        }
        if (psd.children !== undefined) {
            if (env.opts.debug !== undefined) {
                await fs.ensureDir('test');
            }
            for (const child of psd.children) {
                if (child.hidden === true) continue
                simStruct.children.push(this.simplify(child))
            }
            if (env.opts.debug !== undefined) {
                await fs.writeJSON(path.join('test', 'simplified.json'), simStruct, { spaces: 2 })
            }

            for (const child of psd.children) {
                if (child.hidden === true) continue
                await this.collectNodes(psd, instruction, child)
            }
            if (env.opts.debug !== undefined) {
                await fs.writeJSON(path.join('test', 'beforelefttop.json'), simStruct, { spaces: 2 })
            }

            // convert position relative to parent left top
            for (const child of instruction.children) {
                this.toPositionRelativeToParentLeftTop(instruction, child)
            }
            if (env.opts.debug !== undefined) {
                await fs.writeJSON(path.join('test', 'beforeshake.json'), instruction, { spaces: 2 })
            }

            // shake tree
            for (let i = instruction.children.length - 1; i >= 0; i--) {
                const node = instruction.children[i]
                this.treeShake(instruction, i, node)
            }
            if (env.opts.debug !== undefined) {
                await fs.writeJSON(path.join('test', 'beforefixnosize.json'), instruction, { spaces: 2 })
            }

            // convert position relative to parent center
            for (const child of instruction.children) {
                this.toPositionRelativeToParentCenter(instruction, child)
            }
            if (env.opts.debug !== undefined) {
                await fs.writeJSON(path.join('test', 'finished.json'), instruction, { spaces: 2 })
            }
        }
        const prefabRoot = path.join(env.opts.project, env.cfg.PREFAB_ROOT)
        await fs.ensureDir(prefabRoot)
        await fs.writeJSON(path.join(prefabRoot, env.opts.name + '.json'), instruction, { spaces: 2 })
    }

    private simplify (node: Layer): ISimpleNode {
        const snode: ISimpleNode = { name: node.name, left: node.left, top: node.top, right: node.right, bottom: node.bottom, children: [] }
        if (node.children !== undefined) {
            for (let i = node.children.length - 1; i >= 0; i--) {
                const child = node.children[i]
                if (child.hidden === true) {
                    // remove hidden nodes
                    node.children.splice(i, 1)
                    continue
                }
                snode.children.push(this.simplify(child))
            }
        }
        return snode
    }

    private async collectNodes (psd: Psd, parentNode: IBaseNode, layer: Layer): Promise<void> {
        // remove blank layer
        if (layer.children === undefined && layer.canvas === undefined) return

        const left = layer.left ?? 0
        const top = layer.top ?? 0
        const right = layer.right ?? 0
        const bottom = layer.bottom ?? 0
        const x = left // (left + right - psd.width) / 2
        const y = top // -(top + bottom - psd.height) / 2
        const width = right - left
        const height = bottom - top

        let type: PrefabNodeType
        if (layer.text !== undefined && !this.isArtFont(layer)) {
            type = 'Text'
        } else if (layer.canvas !== undefined) {
            type = 'Image'
        } else {
            type = 'GameObject'
        }

        const node: IPrefabNode = {
            type,
            width,
            height,
            x,
            y,
            children: []
        }
        if (layer.name !== undefined) node.name = layer.name
        if (layer.text !== undefined && type === 'Text') {
            const textDetail: IPrefabTextDetail = {
                text: layer.text.text
            }
            if (layer.text.style !== undefined) {
                textDetail.style = this.toITextStyle(layer.text.style, layer.text.transform)
            }
            if (layer.text.styleRuns !== undefined) {
                const styleRuns: IStyleRun[] = []
                for (const sr of layer.text.styleRuns) {
                    styleRuns.push({
                        length: sr.length,
                        style: this.toITextStyle(sr.style)
                    })
                }
                textDetail.styleRuns = styleRuns
            }
            (node as IPrefabText).text = textDetail
        } else if (layer.canvas !== undefined) {
            const layerAlias = layer as ILayerWithCanvas
            (node as IPrefabImage).image = layerAlias.pngFile
        }
        parentNode.children.push(node)

        if (layer.children !== undefined) {
            for (const child of layer.children) {
                if (child.hidden === true) continue
                await this.collectNodes(psd, node, child)
            }
        }
    }

    private treeShake (parent: IBaseNode, index: number, node: IPrefabNode): void {
        for (let i = node.children.length - 1; i >= 0; i--) {
            const child = node.children[i]
            this.treeShake(node, i, child)
        }
        if (node.width === 0 && node.height === 0 && node.type === 'GameObject') {
            if (node.children.length <= 1) {
                // single-child node without size can be shake off
                console.log('shake off:', node.name)
                parent.children.splice(index, 1)
                for (const child of node.children) {
                    child.x += node.x
                    child.y += node.y
                    parent.children.push(child)
                }
            } else {
                // replace with the the first child if it's the biggest
                const first = node.children[0]
                const firstSize = first.width * first.height
                let canShakeOff = true
                for (let i = 1; i < node.children.length; i++) {
                    const child = node.children[i]
                    const childSize = child.width * child.height
                    if (childSize === firstSize) {
                        canShakeOff = false
                        break
                    }
                }
                if (canShakeOff) {
                    console.log('shake off and replace with first child:', node.name)
                    for (let i = 1; i < node.children.length; i++) {
                        const child = node.children[i]
                        child.x -= first.x
                        child.y -= first.y
                        first.children.push(child)
                    }
                    first.x += node.x
                    first.y += node.y
                    parent.children.splice(index, 1, first)
                } else {
                    // set the size and position equal to the first child
                    node.width = first.width
                    node.height = first.height
                    const offsetX = first.x - node.x
                    const offsetY = first.y - node.y
                    node.x = first.x
                    node.y = first.y
                    for (let i = 0; i < node.children.length; i++) {
                        const child = node.children[i]
                        child.x -= offsetX
                        child.y -= offsetY
                    }
                }
            }
        }
    }

    private toPositionRelativeToParentLeftTop (parent: IBaseNode, node: IPrefabNode): void {
        for (const child of node.children) {
            this.toPositionRelativeToParentLeftTop(node, child)
        }
        node.x -= parent.x
        node.y -= parent.y
    }

    private toPositionRelativeToParentCenter (parent: IBaseNode, node: IPrefabNode): void {
        for (const child of node.children) {
            this.toPositionRelativeToParentCenter(node, child)
        }
        node.x = (node.x + node.x + node.width - parent.width) / 2
        node.y = -(node.y + node.y + node.height - parent.height) / 2
    }

    private toITextStyle (style: TextStyle, transform?: number[]): ITextStyle {
        const out: ITextStyle = {}
        if (style.font !== undefined) {
            out.font = style.font.name
        }
        if (style.fontSize !== undefined) {
            let fontSize = style.fontSize
            if (transform !== undefined) {
                // transform is suspected to be [xx, xy, yx, yy, tx, ty]
                fontSize = Math.round((fontSize * transform[3]) * 100) * 0.01
            }
            out.fontSize = fontSize
        }
        if (style.fillColor !== undefined) {
            out.color = style.fillColor
        }
        if (style.strokeColor !== undefined) {
            out.strokeColor = style.strokeColor
        }
        return out
    }

    public isArtFont (layer: Layer): boolean {
        if (layer.canvas !== undefined && layer.text !== undefined && env.cfg.NOT_ART_FONTS !== undefined && layer.text.style?.font?.name !== undefined) {
            return !env.cfg.NOT_ART_FONTS.includes(layer.text.style.font.name)
        }
        return false
    }
}
